package util;
/**
 * Copyright (C) 2008 Universidade Federal de Campina Grande
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

import org.codehaus.plexus.util.IOUtil;

import com.jcraft.jsch.ChannelExec;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;

/**
 * @author thiagoepdc
 * @author Jonhnny Weslley - jonhnny@lsd.ufcg.edu.br
 */
public class Scp extends RemoteService<Scp> {

	enum CopyMode {
		FROM {
			@Override
			void copy(Session session, String sourceFilePath,
					String targetFilePath) throws IOException, JSchException {

				// get I/O streams for remote scp
				OutputStream fos = null;
				OutputStream out = null;
				InputStream in = null;

				try {

					session.connect();

					String command = "scp -f " + sourceFilePath;
					ChannelExec channel = (ChannelExec) session.openChannel("exec");
					channel.setCommand(command);

					out = channel.getOutputStream();
					in = channel.getInputStream();
					channel.connect();

					byte[] buffer = new byte[1024];

					// send '\0'
					buffer[0] = 0;
					out.write(buffer, 0, 1);
					out.flush();

					while (true) {
						int c = checkAck(in);
						if (c != 'C') {
							break;
						}

						// read '0644 '
						in.read(buffer, 0, 5);

						long filesize = 0L;
						while (true) {
							if (in.read(buffer, 0, 1) < 0) {
								// error
								break;
							}
							if (buffer[0] == ' ') {
								break;
							}
							filesize = filesize * 10L + (buffer[0] - '0');
						}

						String file = null;
						for (int i = 0;; i++) {
							in.read(buffer, i, 1);
							if (buffer[i] == (byte) 0x0a) {
								file = new String(buffer, 0, i);
								break;
							}
						}

						// send '\0'
						buffer[0] = 0;
						out.write(buffer, 0, 1);
						out.flush();

						String localFilePath = targetFilePath;
						localFilePath += ((new File(targetFilePath)
								.isDirectory()) ? File.separator + file : "");

						// read a content of targetFile
						fos = new FileOutputStream(localFilePath);
						int length;
						while (true) {
							if (buffer.length < filesize) {
								length = buffer.length;
							} else {
								length = (int) filesize;
							}
							length = in.read(buffer, 0, length);
							if (length < 0) {
								// error
								break;
							}
							fos.write(buffer, 0, length);
							filesize -= length;
							if (filesize == 0L) {
								break;
							}
						}
						fos.close();
						fos = null;

						checkAck(in);

						// send '\0'
						buffer[0] = 0;
						out.write(buffer, 0, 1);
						out.flush();
					}

					session.disconnect();

				} finally {
					IOUtil.close(out);
					IOUtil.close(in);
				}
			}
		},
		TO {
			@Override
			void copy(Session session, String sourceFilePath,
					String targetFilePath) throws IOException, JSchException {

				// get I/O streams for remote scp
				OutputStream out = null;
				InputStream in = null;

				try {

					session.connect();

					String command = "scp -p -t " + targetFilePath;
					ChannelExec channel = (ChannelExec) session.openChannel("exec");
					channel.setCommand(command);

					out = channel.getOutputStream();
					in = channel.getInputStream();
					channel.connect();

					checkAck(in);

					writeHeader(sourceFilePath, out, in);
					writeContent(sourceFilePath, out, in);

					channel.disconnect();
					session.disconnect();

				} finally {
					IOUtil.close(out);
					IOUtil.close(in);
				}
			}

			/**
			 * @param file
			 * @param out
			 * @param in
			 * @throws IOException
			 */
			private void writeHeader(String file, OutputStream out,
					InputStream in) throws IOException {
				/*
				 * send "C0644 filesize filename",
				 * where filename should not include '/'
				 */
				long filesize = new File(file).length();

				String command = "C0644 " + filesize + " ";

				if (file.lastIndexOf('/') > 0) {
					command += file.substring(file.lastIndexOf('/') + 1);
				} else {
					command += file;
				}

				command += "\n";
				out.write(command.getBytes());
				out.flush();

				checkAck(in);
			}

			/**
			 * @param file
			 * @param out
			 * @param in
			 *
			 * @throws IOException
			 */
			private void writeContent(String file, OutputStream out,
					InputStream in) throws IOException {
				/*
				 * None of the Streams are closed here. Exceptions do not matter
				 * here. The close call is made by caller method
				 */

				// send a content of file
				FileInputStream fis = new FileInputStream(file);

				byte[] buffer = new byte[1024];
				int dataRead = 0;
				while ((dataRead = fis.read(buffer)) != -1) {
					out.write(buffer, 0, dataRead); // out.flush();
				}

				buffer[0] = 0; // send '\0'
				out.write(buffer, 0, 1);
				out.flush();

				checkAck(in);
			}

		};

		abstract void copy(Session session, String sourceFilePath,
				String targetFilePath) throws IOException, JSchException;

		/**
		 * @param in
		 *
		 * @throws IOException
		 */
		int checkAck(InputStream in) throws IOException {
			int errorCode = in.read();
			if ((errorCode == 1) || (errorCode == 2)) {
				StringBuffer sb = new StringBuffer();
				int c;
				do {
					c = in.read();
					sb.append((char) c);
				} while (c != '\n');
				throw new AckException(sb.toString());
			}
			return errorCode;
		}

	}

	private final CopyMode mode;

	public Scp() {
		this(CopyMode.TO);
	}

	public Scp(CopyMode mode) {
		this.mode = mode;
	}

	/**
	 * Copies files between hosts on a network.
	 *
	 * @param sourceFilePath
	 *            the source file
	 * @param targetFilePath
	 *            the target file
	 *
	 * @return
	 * @throws IOException
	 * @throws JSchException
	 */
	public void copy(String sourceFilePath, String targetFilePath)
			throws IOException {
		try {
			mode.copy(createSession(), sourceFilePath, targetFilePath);

		} catch (JSchException e) {
			throw new IOException(e);
		}
	}

	public static Scp from(String hostname) {
		return new Scp(CopyMode.FROM).at(hostname);
	}

	public static Scp to(String hostname) {
		return new Scp(CopyMode.TO).at(hostname);
	}

}